import { CompilerOptions, DiagnosticCategory, ModuleKind, Project, ScriptTarget, SourceFile } from 'ts-morph';
import { PluginError } from './types';

/**
 * Creates a TS code generation project
 */
export function createProject(options?: CompilerOptions) {
    return new Project({
        compilerOptions: {
            target: ScriptTarget.ES2016,
            module: ModuleKind.CommonJS,
            esModuleInterop: true,
            declaration: true,
            strict: true,
            skipLibCheck: true,
            noEmitOnError: true,
            noImplicitAny: false,
            skipDefaultLibCheck: true,
            types: ['node'],
            ...options,
        },
    });
}

export function saveSourceFile(sourceFile: SourceFile) {
    sourceFile.replaceWithText(
        `/******************************************************************************
 * This file was generated by ZenStack CLI.
 ******************************************************************************/

/* eslint-disable */

    ${sourceFile.getText()}`
    );
    sourceFile.formatText();
    sourceFile.saveSync();
}

/**
 * Persists a TS project to disk.
 */
export async function saveProject(project: Project) {
    project.getSourceFiles().forEach(saveSourceFile);
    await project.save();
}

/**
 * Emit a TS project to JS files.
 */
export async function emitProject(project: Project) {
    const errors = project.getPreEmitDiagnostics().filter((d) => d.getCategory() === DiagnosticCategory.Error);
    if (errors.length > 0) {
        console.error('Error compiling generated code:');
        console.error(project.formatDiagnosticsWithColorAndContext(errors.slice(0, 10)));
        await project.save();
        throw new PluginError('', `Error compiling generated code`);
    }

    const result = await project.emit();

    const emitErrors = result.getDiagnostics().filter((d) => d.getCategory() === DiagnosticCategory.Error);
    if (emitErrors.length > 0) {
        console.error('Some generated code is not emitted:');
        console.error(project.formatDiagnosticsWithColorAndContext(emitErrors.slice(0, 10)));
        await project.save();
        throw new PluginError('', `Error emitting generated code`);
    }
}

/*
 * Abstraction for source code writer.
 */
export interface CodeWriter {
    block(callback: () => void): void;
    inlineBlock(callback: () => void): void;
    write(text: string): void;
    writeLine(text: string): void;
    conditionalWrite(condition: boolean, text: string): void;
}

/**
 * A fast code writer.
 */
export class FastWriter implements CodeWriter {
    private content = '';
    private indentLevel = 0;

    constructor(private readonly indentSize = 4) {}

    get result() {
        return this.content;
    }

    block(callback: () => void) {
        this.content += '{\n';
        this.indentLevel++;
        callback();
        this.indentLevel--;
        this.content += '\n}';
    }

    inlineBlock(callback: () => void) {
        this.content += '{';
        callback();
        this.content += '}';
    }

    write(text: string) {
        this.content += this.indent(text);
    }

    writeLine(text: string) {
        this.content += `${this.indent(text)}\n`;
    }

    conditionalWrite(condition: boolean, text: string) {
        if (condition) {
            this.write(text);
        }
    }

    private indent(text: string) {
        return ' '.repeat(this.indentLevel * this.indentSize) + text;
    }
}
